Skip to content

feat: add Linux system-wide setup#376

Closed
Sahilb315 wants to merge 23 commits into
mainfrom
cursor/linux-system-install
Closed

feat: add Linux system-wide setup#376
Sahilb315 wants to merge 23 commits into
mainfrom
cursor/linux-system-install

Conversation

@Sahilb315

@Sahilb315 Sahilb315 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds Linux system-wide setup so one install protects every user account on shared VMs and golden Docker images.

  • pmg setup install --system (Linux, root): package-manager shims in /usr/local/lib/pmg/bin, managed config at /etc/safedep/pmg/config.yml, login-shell PATH via /etc/profile.d/pmg.sh. pmg setup remove --system uninstalls; removal is best-effort so one failed step does not strand the rest.
  • The pmg binary is validated before install: root-owned, world-executable, not writable by group or others, and in a root-owned, non-world-writable directory. Group-writable parents are tolerated because Debian/Ubuntu ship /usr/local/bin as root:staff 2775.
  • Per-user paths (event logs, cloud-sync WAL, cache) resolve from root's passwd home when running as root, never from $HOME. Environments where sudo/su preserve the invoking user's HOME previously made any sudo pmg run create root-owned state inside that user's home, fail-closing every later non-root pmg/npm/pip run for them.
  • Event-log init failure stays fatal (no silent audit loss) and exits with a triaged remedy: restore ownership with chown when another account created files inside your own home, fix the environment when a leaked HOME/XDG_CONFIG_HOME points at another user's home (chown there would brick the other account), make the directory writable when PMG_CONFIG_DIR is explicit.
  • pmg setup doctor understands system installs: alias and shim checks, a resolution-based PATH check that accepts either shim directory, and an event-log writability probe with the same triaged fix.
  • Cloud events from sudo runs are attributed to the invoking user via SUDO_USER.
  • E2E job for system install: binary rejection cases, install artifacts, PATH activation, reason-checked managed-config refusal, a regression assertion that root runs leave user homes untouched, and non-root interception through system shims.
  • Docs: docs/system-install.md covers setup, Docker ENV PATH, limitations, user data directories, and troubleshooting.

Made with Cursor

@codecov-commenter

codecov-commenter commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 47.51381% with 285 lines in your changes missing coverage. Please review.
✅ Project coverage is 55.72%. Comparing base (d3e656e) to head (4d65a2c).

Files with missing lines Patch % Lines
cmd/setup/doctor.go 41.84% 78 Missing and 4 partials ⚠️
cmd/setup/setup.go 16.09% 72 Missing and 1 partial ⚠️
internal/shim/system.go 66.22% 41 Missing and 10 partials ⚠️
config/config.go 68.00% 15 Missing and 9 partials ⚠️
internal/shim/shim.go 51.42% 11 Missing and 6 partials ⚠️
main.go 0.00% 13 Missing ⚠️
cmd/setup/info.go 0.00% 12 Missing ⚠️
internal/ui/info.go 0.00% 9 Missing ⚠️
internal/audit/cloud_sink.go 84.61% 1 Missing and 1 partial ⚠️
internal/shim/file_owner_unix.go 60.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #376      +/-   ##
==========================================
- Coverage   55.78%   55.72%   -0.07%     
==========================================
  Files         217      219       +2     
  Lines       14781    15249     +468     
==========================================
+ Hits         8246     8497     +251     
- Misses       5790     5978     +188     
- Partials      745      774      +29     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sahilb315 and others added 3 commits July 11, 2026 02:19
Install shared shims, managed configuration, and login-shell PATH integration so golden images and multi-user hosts can protect package installs for every user.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Tighten shim detection, profile repair, and install ordering while
trimming over-specific doctor/info hints from the system-install path.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Sahilb315 Sahilb315 force-pushed the cursor/linux-system-install branch from 3635f35 to ffd0e7e Compare July 10, 2026 20:52
Sahilb315 and others added 2 commits July 13, 2026 15:37
Use UserBinDir for PATH checks and pass aliases as not required under
system install without treating that as active interception.

Co-authored-by: Cursor <cursoragent@cursor.com>
Prefix the warning with [pmg] and drop the redundant continuing clause.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Sahilb315 Sahilb315 marked this pull request as ready for review July 13, 2026 10:15
@safedep

safedep Bot commented Jul 13, 2026

Copy link
Copy Markdown

SafeDep Report Summary

Green Malicious Packages Badge Green Vulnerable Packages Badge Green Risky License Badge

Package Details
Package Malware Vulnerability Risky License Report
icon buf.build/gen/go/safedep/api/protocolbuffers/go @ v1.36.11-20260713161921-716fa3011a21.1
go.mod
ok icon
ok icon
ok icon
🔗

View complete scan results →

This report is generated by SafeDep Github App

@Sahilb315 Sahilb315 requested a review from abhisek July 13, 2026 10:29
Comment thread docs/config.md
Comment thread docs/system-install.md
Sahilb315 and others added 4 commits July 13, 2026 17:02
Cover root system setup, PATH/profile.d, managed config, non-root
interception, and remove. Pin pnpm 11.10.0 on the package-manager e2e
job after an integrity crash on pnpm add.

Co-authored-by: Cursor <cursoragent@cursor.com>
Align package.json with the pnpm version we want in CI so action-setup
stops erroring on a version mismatch after the e2e integrity flake.

Co-authored-by: Cursor <cursoragent@cursor.com>
pnpm 11.x `pnpm init` still writes onFail:download; `pnpm add` then
fails after PMG analysis even on 11.10.0. Seed the temp package with
npm init instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
The e2e integrity crash is avoided by npm init; the 11.10.0 bump is
no longer needed.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Sahilb315 Sahilb315 requested a review from abhisek July 13, 2026 12:12
Comment thread main.go

@abhisek abhisek left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep-dive review of the system-install feature. Overall: the design is sound and rides well on existing infrastructure (managed/global config, PMG_SHIM_PATH stripping), the docs are genuinely good, and the new e2e-system-install CI job is a strong addition — it covers the flows unit tests can't reach, including non-root interception through the system shims and clean removal. Idempotent install with profile self-repair, usefulerror codes for the root/platform gates, and the install-order comment in installSystem are all appreciated.

Issues, in priority order (details inline):

  1. Event-logging fail-open is a global policy change — explicit --log failures should stay fatal, and lockdown (IsLocked()) should keep enforcement; soft-fail is right as the default. (main.go)
  2. validateSystemExecutable only checks o+x — a user-writable binary path baked into root-installed shims is a lateral-movement vector on multi-user machines. (internal/shim/system.go)
  3. remove --system is blocked by the same validation — uninstall should not require a valid executable. (cmd/setup/setup.go)
  4. PATH precedence isn't verified — nvm/pyenv-style rc prepends silently bypass system shims while doctor reports green on membership. (cmd/setup/doctor.go, docs)
  5. isInterceptionActive branches on a display string — message text is now load-bearing logic, also grepped by CI. (cmd/setup/doctor.go)
  6. Smaller items: install output should mention that per-user configs become ignored; writeSystemProfile reads the global bin dir instead of the manager's config.

Nits not worth inline threads: errIfSystemInstallAllowed reads as "error if allowed" — consider requireSystemInstallSupported; per-user Remove() now aborts before rc cleanup when shim-dir removal fails (consider attempting rc cleanup and joining errors); the pnpm-init CI flake fix is unrelated to this feature and could land separately.

I'd hold merge on 1 and 2; the rest is follow-up-able.


Generated by Claude Code

Comment thread main.go Outdated
Comment thread internal/shim/system.go
Comment thread cmd/setup/setup.go Outdated
Comment thread cmd/setup/doctor.go Outdated
Comment thread cmd/setup/doctor.go Outdated
Comment thread internal/ui/info.go
Comment thread internal/shim/system.go Outdated
Sahilb315 and others added 2 commits July 13, 2026 22:20
Require root-owned, non-group/other-writable pmg for --system install;
allow remove without that validation. Doctor checks npm resolution for
PATH precedence, uses ImpliesInterception instead of message matching,
and documents version-manager shadowing. Pass profile bin dir from the
shim manager and note that system config ignores per-user files.

Co-authored-by: Cursor <cursoragent@cursor.com>
Doctor now verifies every installed package manager against the shim
directory, and system-install validation only requires a safe parent
directory. Cloud sync records username/uid on invocation context for
multi-user hosts sharing one endpoint.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown

vet Summary Report

This report is generated by vet

Policy Checks

  • ✅ Vulnerability
  • ✅ Malware
  • ✅ License
  • ✅ Popularity
  • ✅ Maintenance
  • ✅ Security Posture
  • ✅ Threats

Malicious Package Analysis

Malicious package analysis was performed using SafeDep Cloud API

Malicious Package Analysis Report
Ecosystem Package Version Status Report
  • ℹ️ 0 packages have been actively analyzed for malicious behaviour.
  • ✅ No malicious packages found.

Note: Some of the package analysis jobs may still be running.Please check back later. Consider increasing the timeout for better coverage.

Changed Packages

Changed Packages

  • ✅ [Go] buf.build/gen/go/safedep/api/protocolbuffers/go@1.36.11-20260713161921-716fa3011a21.1

- shim: make system executable resolution injectable so tests pass under
  umask 002; skip the root-owner test when running as root
- doctor: treat resolution into either the system or per-user shim dir as
  intercepted, and collapse the shim-in-PATH check to a single call site
- setup: make remove (both --system and per-user) best-effort with
  errors.Join so one failed step no longer strands the other artifact
- shim: allow a group-writable install parent dir (Debian/Ubuntu ship
  /usr/local/bin as root:staff 2775) while still rejecting world-writable
  and non-root-owned parents
- audit: attribute cloud events to SUDO_USER when running under sudo
- docs: drop the soft-fail event-logging claim (hard-fail is retained)
The GitHub ubuntu-latest runner ships /usr/local/bin world-writable so
tooling can install without sudo. System install correctly refuses a
world-writable dir for the shared binary (any local user could replace
it and hijack every user's npm/pip). No FHS-compliant distro or Docker
image ships it world-writable — it is always root:root 0755 or
root:staff 2775 — so this normalizes only the anomalous CI runner back
to standard perms and still exercises the real /usr/local/bin path.
A pmg run as root with a preserved HOME (GitHub runners, sudo -E, su
without -) creates the invoking user's ~/.config/safedep as root-owned,
and event-log init then fail-closes every later non-root command.
Make that state self-solvable:

- event-log init permission errors exit with a usefulerror naming the
  likely cause and the chown fix instead of a bare fatal
- pmg setup doctor probes event-log dir writability and reports the
  same fix via a new per-result Fix override
- document the mechanism and remedy in system-install.md, along with
  the binary ownership requirements for --system
- consolidate this branch's doctor tests into doctor_test.go
Path resolution trusted HOME (and XDG_*), which sudo and su can preserve
from the invoking user (GitHub runners, sudo -E, su without -). Any pmg
run as root then created root-owned ~/.config/safedep inside that user's
home, and event-log init fail-closed every later non-root pmg/npm/pip
run for them. System install made sudo pmg the documented flow, turning
this latent bug into the happy path.

When euid is 0, configDir and cacheDir now resolve from root's passwd
home instead of the environment, so root state lands under /root and
user homes are never touched. PMG_CONFIG_DIR/PMG_CACHE_DIR still win,
non-root resolution is unchanged, and Windows is unaffected (no euid).
Event-log init stays fatal on failure; sudo-run package events are
attributed via SUDO_USER and synced by the exit auto-sync as usual.

E2E: GitHub runners preserve HOME under sudo, so assert that no sudo
pmg run leaks state into the runner's home, and that the managed-config
refusal fails for the documented reason rather than a permission brick.
The chown hint is only correct when another account created files
inside the current user's own home. When a leaked HOME or
XDG_CONFIG_HOME points at another user's home (e.g. sudo -u on GitHub
runners), following it would chown that user's directory and brick
their pmg instead. Classify the failure against the passwd home,
which the leaked environment cannot influence, and prescribe:

- dir inside own home: restore ownership with chown
- dir outside own home: fix the leaked environment, never chown
- explicit PMG_CONFIG_DIR: make it writable

Used by both the fatal event-log error and the doctor check, and the
docs troubleshooting now carries the same two-case triage.
GitHub runners export XDG_CONFIG_HOME=/home/runner/.config and it leaks
through sudo -u, so the pmgtest pmg resolved the runner user's config
dir and fail-closed on its runner-owned log file (run 29289868727 shows
the triaged error catching exactly this). Set it inside the login shell
so it wins regardless of how the leak is delivered.

The remedy now returns a full-help and doctor-table pair from a single
triage, and drops the do-not-chown tail from the leak message.
Belongs with the previous commit; it was left unstaged and e0580b1
does not compile without it.
Running as uid 0 without a resolvable root passwd entry (scratch
containers, minimal chroots) panicked at startup on every command,
because the euid-based path resolution had no fallback. Fall back to
env-derived resolution there: without a passwd database there is no
user switching, so the cross-user poisoning that branch prevents
cannot occur.

Also restore the underlying cause in the generic event-log init error
(minimal output hid it after the usefulerror change), and document
that root's per-user data lives under /root regardless of a preserved
HOME.
@Sahilb315 Sahilb315 requested a review from abhisek July 13, 2026 23:22
Comment thread internal/audit/cloud_sink.go Outdated
Comment thread internal/shim/system.go

abhisek commented Jul 14, 2026

Copy link
Copy Markdown
Member

Follow-up on the latest push (297f516): the new root path-resolution branch breaks two existing config tests when the suite runs as root.

configDir() / cacheDir() now skip os.UserConfigDir() / os.UserCacheDir() entirely when configGeteuid() == 0 and resolve from root's passwd home instead — so HOME and XDG_* are no longer consulted for root. TestSandboxProfileDirRespectsXDGConfigHome and TestSandboxViolationCacheDirRespectsXDGCacheHome set XDG_* via t.Setenv and assert the path lands there, without stubbing euid (before this PR, euid was irrelevant to path resolution).

Verified with an A/B run in the same root container:

  • origin/main: both tests pass as root
  • PR head 297f516: both fail as root (expected the XDG temp dir, got /root/.cache/safedep/pmg/...)

CI stays green only because GitHub-hosted runners run as non-root runner — any Docker-based CI, act, or root dev container hits this.

Two ways to fix, depending on the intended contract:

  1. Keep the new semantics (root always uses passwd home; only PMG_CONFIG_DIR/PMG_CACHE_DIR override): stub withEuid(t, 1000) in those two tests, same as config/rootdir_test.go already does — two-line fix. Then document that root ignores HOME/XDG_*, since it's now intentional behavior.
  2. Soften (root honors explicitly set XDG_*, distrusts only preserved HOME): tests pass unchanged, but this partially reopens the sudo-env-poisoning hole the branch exists to close, since sudo preserves XDG_* just as easily as HOME.

Option 1 seems right — it matches the PR's threat model and its own e2e step (which fixes leaked env for the non-root user, where env is still honored). Might also be worth adding a root go test ./config run (e.g. in a container) to CI, since euid-dependent behavior is now permanent and GitHub runners never exercise the root branch.


Generated by Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a Linux “system-wide install” path so a single PMG installation can protect all users on shared VMs and baked images, with stronger install-time validation and better diagnostics for common sudo/root environment pitfalls.

Changes:

  • Introduces pmg setup install --system / pmg setup remove --system (Linux, root) with system shim/profile management and managed config at OS paths.
  • Hardens root-run behavior around per-user state location (config/cache) and improves fatal event-log init errors plus pmg setup doctor checks/remediation.
  • Adds CI/E2E coverage for system install flows and updates docs to cover usage, Docker implications, and troubleshooting.

Reviewed changes

Copilot reviewed 30 out of 31 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
README.md Adds a pointer to Linux system-wide setup docs.
main.go Converts event-log init failures into categorized usefulerror with a permission-specific remedy.
internal/ui/info.go Adds post-install UX output for system-wide setup (paths + Docker PATH hint).
internal/shim/system.go Implements system shim manager, executable safety validation, and /etc/profile.d snippet management.
internal/shim/system_test.go Adds unit tests for system shim install/remove, profile writing, and executable validation.
internal/shim/shim.go Extends shim config for system mode (skip rc edits/manage profile), best-effort removal, and shim detection helpers.
internal/shim/shim_test.go Adds coverage for error-returning behavior in Remove().
internal/shim/path.go Ensures PATH filtering strips the system shim directory too.
internal/shim/path_test.go Adds a test for stripping the system shim dir without env vars.
internal/shim/file_owner_windows.go Adds Windows stub for owner UID resolution.
internal/shim/file_owner_unix.go Adds Unix owner UID resolution for executable ownership checks.
internal/doctor/doctor.go Extends doctor results with ImpliesInterception and per-result fix overrides.
internal/audit/cloud_sink.go Attributes cloud events to the invoking user (prefers SUDO_USER).
internal/audit/cloud_sink_test.go Asserts username/uid are present in emitted invocation context.
go.work.sum Updates dependency checksums.
go.sum Updates dependency checksums for the bumped API protos.
go.mod Bumps buf.build/gen/go/.../protocolbuffers module version.
docs/system-install.md New documentation for Linux system install, Docker PATH, limitations, and troubleshooting.
docs/persistent-proxy.md Updates system-install reference to point at the new doc.
docs/development.md Adds system-install doc to the development docs index.
docs/config.md Links to system install doc from config docs.
config/rootdir_test.go Tests root path resolution ignoring poisoned HOME/XDG env vars.
config/remedy_test.go Tests triaged help text for unwritable config/event-log directories.
config/managed_config_test.go Adds tests for writing/removing the system-managed template config.
config/config.go Implements root config/cache dir resolution from passwd home, triaged remedies, and system-managed config helpers.
cmd/setup/setup.go Adds --system install/remove modes, root requirement enforcement, and best-effort removal behavior.
cmd/setup/setup_test.go Adds tests for Linux/root gating on --system.
cmd/setup/info.go Extends setup info output to include system shim state.
cmd/setup/doctor.go Updates doctor to understand system installs, PATH resolution, and triaged event-log writability fixes.
cmd/setup/doctor_test.go Adds targeted unit tests for new doctor resolution logic and interception inference.
.github/workflows/pmg-e2e.yml Adds a dedicated E2E job for system install (binary rejection, artifacts, PATH activation, managed config, non-root interception, uninstall).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/shim/system_test.go
Comment thread internal/audit/cloud_sink.go
Comment thread docs/system-install.md Outdated
Comment thread cmd/setup/setup.go
…check

Address review findings on the system-install PR:

- cloud_sink: honor SUDO_USER for audit attribution only when euid==0.
  Without the gate any user could set SUDO_USER and spoof cloud-audit
  attribution to another account. Matches the guard in cmd/setup/cert.go.

- config: divert per-user paths to root's passwd home only on an actual
  sudo elevation (euid==0 && SUDO_USER set), not for every root euid.
  The blanket root diversion ignored HOME/XDG_CONFIG_HOME and silently
  stopped reading genuine root users' config (golden Docker images),
  regressing two tests that only fail when the suite runs as root.
  Genuine root honors the environment as before; su without - leaves no
  marker and stays a documented, loud-failing residual.

- doctor: add a system-only check re-validating that the binary the
  installed shims exec is still root-owned and non-writable, catching
  permission/ownership drift after install.

- shim: fold the duplicated shim-scan loop into firstShimContent.
Address remaining review comments:

- shim: force root:root 0755 on the managed system dirs (shim tree and
  profile.d) after MkdirAll. A pre-created dir with weaker ownership,
  possible under Debian's group-writable /usr/local/lib, would let a
  non-root user replace the shims every account executes.

- audit: when SUDO_USER has no passwd entry (minimal containers),
  attribute cloud events from sudo's recorded SUDO_USER/SUDO_UID env
  instead of falling back to root. Still gated on euid 0.

- setup: reword the root-without---system warning; alias/shim install
  follows HOME, so claiming it configures only root's home was wrong.

- shim: skip the non-root-owner validation test on Windows, where file
  ownership is not resolvable.
The system-install validation checked the binary's own permissions and
the parent's tamper-safety but never reachability: a 0755 root-owned
binary under a 0700 directory (e.g. /root/pmg) passed every check while
every non-root user's shim failed with exit 127. Walk the directory
chain to / and require the search bit for others; doctor's system
binary check inherits this. E2E gains a reject case for a binary under
a non-searchable directory.

setup info: render alias/user-shim/system-shim rows through one
installed-state formatter (location when installed, "not installed"
otherwise) instead of a mix of booleans, paths, and prose.
writeSystemProfile chowned/chmod'd /etc/profile.d itself, a shared
system directory pmg does not own, silently overriding any perms a
sysadmin set on it. Secure only the file pmg writes (pmg.sh) via
secureSystemFile, which also forces 0644 explicitly so a restrictive
root umask cannot leave the snippet non-world-readable (which would
drop the shim dir from other users' login-shell PATH).

Docs: add Limitations entries for the group-writable install dir
bypass (validation is defeatable on non-sticky group-writable dirs
like Debian's /usr/local/bin) and the elevation-only scope (su without
- can still poison the caller's home; sudo -u cannot poison another
account). Trim the requireSafeParentDir comment to a pointer.
Comment thread config/config.go
// realUserHomeDir returns the current user's home from the passwd database,
// ignoring HOME and XDG_* env vars that may be leaked from another account.
// Overridable in tests.
var realUserHomeDir = func() (string, error) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is ambiguous. What is "real"? It should be "currentUserHomeDir"

Comment thread config/config.go
if err != nil {
return "", err
}
return u.HomeDir, nil

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check and fail if u.HomeDir is empty?

Comment thread config/config.go
// belongs to another account steals it and bricks that account instead, so
// its is only suggested when the directory is inside the current user's
// real (passwd) home, which a leaked environment cannot influence.
func UnwritableConfigDirRemedy(dir string) (help, fix string) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this function? Only to return some remedy? If "dir" is the target, then why is it touching pmgConfigDirEnvKey, current user home dir?

From the description, the function looks overloaded and violates SRP:

returns actionable help for a per-user config or event-log directory

abhisek pushed a commit that referenced this pull request Jul 14, 2026
Address the open review threads on #376:
- rename realUserHomeDir to currentUserHomeDir and fail when the passwd
  entry has no home directory
- split UnwritableConfigDirRemedy into classifyUnwritableDir (cause
  diagnosis) and pure message rendering so each function has one job
- replace cmd/setup's duplicate pathIsUnderDir with the shared
  config.PathWithinDir, now guarding empty inputs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rWmR6NS47FpedJsrTWk8A
Sahilb315 added a commit that referenced this pull request Jul 14, 2026
* feat: add Linux system-wide setup

Install shared shims, managed configuration, and login-shell PATH integration so golden images and multi-user hosts can protect package installs for every user.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: keep local design documents untracked

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: harden and simplify Linux system install

Tighten shim detection, profile repair, and install ordering while
trimming over-specific doctor/info hints from the system-install path.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: clarify system-install doctor alias and shim path checks

Use UserBinDir for PATH checks and pass aliases as not required under
system install without treating that as active interception.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: tighten event-log soft-fail warning prefix

Prefix the warning with [pmg] and drop the redundant continuing clause.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci: add Linux system-install e2e and pin pnpm for add flake

Cover root system setup, PATH/profile.d, managed config, non-root
interception, and remove. Pin pnpm 11.10.0 on the package-manager e2e
job after an integrity crash on pnpm add.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: bump packageManager to pnpm 11.10.0 for e2e

Align package.json with the pnpm version we want in CI so action-setup
stops erroring on a version mismatch after the e2e integrity flake.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci: use npm init for pnpm e2e to avoid integrity crash

pnpm 11.x `pnpm init` still writes onFail:download; `pnpm add` then
fails after PMG analysis even on 11.10.0. Seed the temp package with
npm init instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: revert packageManager pin to pnpm 11.1.3

The e2e integrity crash is avoided by npm init; the 11.10.0 bump is
no longer needed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: harden system-install review findings

Require root-owned, non-group/other-writable pmg for --system install;
allow remove without that validation. Doctor checks npm resolution for
PATH precedence, uses ImpliesInterception instead of message matching,
and documents version-manager shadowing. Pass profile bin dir from the
shim manager and note that system config ignores per-user files.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: harden doctor PATH checks and attribute cloud events by OS user

Doctor now verifies every installed package manager against the shim
directory, and system-install validation only requires a safe parent
directory. Cloud sync records username/uid on invocation context for
multi-user hosts sharing one endpoint.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: address system-install review findings

- shim: make system executable resolution injectable so tests pass under
  umask 002; skip the root-owner test when running as root
- doctor: treat resolution into either the system or per-user shim dir as
  intercepted, and collapse the shim-in-PATH check to a single call site
- setup: make remove (both --system and per-user) best-effort with
  errors.Join so one failed step no longer strands the other artifact
- shim: allow a group-writable install parent dir (Debian/Ubuntu ship
  /usr/local/bin as root:staff 2775) while still rejecting world-writable
  and non-root-owned parents
- audit: attribute cloud events to SUDO_USER when running under sudo
- docs: drop the soft-fail event-logging claim (hard-fail is retained)

* ci: normalize /usr/local/bin perms before system-install e2e

The GitHub ubuntu-latest runner ships /usr/local/bin world-writable so
tooling can install without sudo. System install correctly refuses a
world-writable dir for the shared binary (any local user could replace
it and hijack every user's npm/pip). No FHS-compliant distro or Docker
image ships it world-writable — it is always root:root 0755 or
root:staff 2775 — so this normalizes only the anomalous CI runner back
to standard perms and still exercises the real /usr/local/bin path.

* fix: actionable remedy for root-created per-user config dir

A pmg run as root with a preserved HOME (GitHub runners, sudo -E, su
without -) creates the invoking user's ~/.config/safedep as root-owned,
and event-log init then fail-closes every later non-root command.
Make that state self-solvable:

- event-log init permission errors exit with a usefulerror naming the
  likely cause and the chown fix instead of a bare fatal
- pmg setup doctor probes event-log dir writability and reports the
  same fix via a new per-result Fix override
- document the mechanism and remedy in system-install.md, along with
  the binary ownership requirements for --system
- consolidate this branch's doctor tests into doctor_test.go

* fix: resolve per-user paths from root's own home when running as root

Path resolution trusted HOME (and XDG_*), which sudo and su can preserve
from the invoking user (GitHub runners, sudo -E, su without -). Any pmg
run as root then created root-owned ~/.config/safedep inside that user's
home, and event-log init fail-closed every later non-root pmg/npm/pip
run for them. System install made sudo pmg the documented flow, turning
this latent bug into the happy path.

When euid is 0, configDir and cacheDir now resolve from root's passwd
home instead of the environment, so root state lands under /root and
user homes are never touched. PMG_CONFIG_DIR/PMG_CACHE_DIR still win,
non-root resolution is unchanged, and Windows is unaffected (no euid).
Event-log init stays fatal on failure; sudo-run package events are
attributed via SUDO_USER and synced by the exit auto-sync as usual.

E2E: GitHub runners preserve HOME under sudo, so assert that no sudo
pmg run leaks state into the runner's home, and that the managed-config
refusal fails for the documented reason rather than a permission brick.

* fix: triage the unwritable config dir remedy by cause

The chown hint is only correct when another account created files
inside the current user's own home. When a leaked HOME or
XDG_CONFIG_HOME points at another user's home (e.g. sudo -u on GitHub
runners), following it would chown that user's directory and brick
their pmg instead. Classify the failure against the passwd home,
which the leaked environment cannot influence, and prescribe:

- dir inside own home: restore ownership with chown
- dir outside own home: fix the leaked environment, never chown
- explicit PMG_CONFIG_DIR: make it writable

Used by both the fatal event-log error and the doctor check, and the
docs troubleshooting now carries the same two-case triage.

* ci: pin XDG_CONFIG_HOME for the cross-user e2e step; terse doctor fix

GitHub runners export XDG_CONFIG_HOME=/home/runner/.config and it leaks
through sudo -u, so the pmgtest pmg resolved the runner user's config
dir and fail-closed on its runner-owned log file (run 29289868727 shows
the triaged error catching exactly this). Set it inside the login shell
so it wins regardless of how the leak is delivered.

The remedy now returns a full-help and doctor-table pair from a single
triage, and drops the do-not-chown tail from the leak message.

* fix: adapt event-log error to the two-value remedy signature

Belongs with the previous commit; it was left unstaged and e0580b1
does not compile without it.

* fix: fall back to env path resolution when root has no passwd entry

Running as uid 0 without a resolvable root passwd entry (scratch
containers, minimal chroots) panicked at startup on every command,
because the euid-based path resolution had no fallback. Fall back to
env-derived resolution there: without a passwd database there is no
user switching, so the cross-user poisoning that branch prevents
cannot occur.

Also restore the underlying cause in the generic event-log init error
(minimal output hid it after the usefulerror change), and document
that root's per-user data lives under /root regardless of a preserved
HOME.

* fix: gate SUDO_USER trust and root path diversion; add doctor binary check

Address review findings on the system-install PR:

- cloud_sink: honor SUDO_USER for audit attribution only when euid==0.
  Without the gate any user could set SUDO_USER and spoof cloud-audit
  attribution to another account. Matches the guard in cmd/setup/cert.go.

- config: divert per-user paths to root's passwd home only on an actual
  sudo elevation (euid==0 && SUDO_USER set), not for every root euid.
  The blanket root diversion ignored HOME/XDG_CONFIG_HOME and silently
  stopped reading genuine root users' config (golden Docker images),
  regressing two tests that only fail when the suite runs as root.
  Genuine root honors the environment as before; su without - leaves no
  marker and stays a documented, loud-failing residual.

- doctor: add a system-only check re-validating that the binary the
  installed shims exec is still root-owned and non-writable, catching
  permission/ownership drift after install.

- shim: fold the duplicated shim-scan loop into firstShimContent.

* fix: harden system dirs at install; keep sudo attribution without passwd

Address remaining review comments:

- shim: force root:root 0755 on the managed system dirs (shim tree and
  profile.d) after MkdirAll. A pre-created dir with weaker ownership,
  possible under Debian's group-writable /usr/local/lib, would let a
  non-root user replace the shims every account executes.

- audit: when SUDO_USER has no passwd entry (minimal containers),
  attribute cloud events from sudo's recorded SUDO_USER/SUDO_UID env
  instead of falling back to root. Still gated on euid 0.

- setup: reword the root-without---system warning; alias/shim install
  follows HOME, so claiming it configures only root's home was wrong.

- shim: skip the non-root-owner validation test on Windows, where file
  ownership is not resolvable.

* fix: reject system binaries unreachable by other users; consistent info

The system-install validation checked the binary's own permissions and
the parent's tamper-safety but never reachability: a 0755 root-owned
binary under a 0700 directory (e.g. /root/pmg) passed every check while
every non-root user's shim failed with exit 127. Walk the directory
chain to / and require the search bit for others; doctor's system
binary check inherits this. E2E gains a reject case for a binary under
a non-searchable directory.

setup info: render alias/user-shim/system-shim rows through one
installed-state formatter (location when installed, "not installed"
otherwise) instead of a mix of booleans, paths, and prose.

* fix: stop reowning /etc/profile.d; document group-writable and su gaps

writeSystemProfile chowned/chmod'd /etc/profile.d itself, a shared
system directory pmg does not own, silently overriding any perms a
sysadmin set on it. Secure only the file pmg writes (pmg.sh) via
secureSystemFile, which also forces 0644 explicitly so a restrictive
root umask cannot leave the snippet non-world-readable (which would
drop the shim dir from other users' login-shell PATH).

Docs: add Limitations entries for the group-writable install dir
bypass (validation is defeatable on non-sticky group-writable dirs
like Debian's /usr/local/bin) and the elevation-only scope (su without
- can still poison the caller's home; sudo -u cannot poison another
account). Trim the requireSafeParentDir comment to a pointer.

* refactor: separate unwritable-dir diagnosis from remedy rendering

Address the open review threads on #376:
- rename realUserHomeDir to currentUserHomeDir and fail when the passwd
  entry has no home directory
- split UnwritableConfigDirRemedy into classifyUnwritableDir (cause
  diagnosis) and pure message rendering so each function has one job
- replace cmd/setup's duplicate pathIsUnderDir with the shared
  config.PathWithinDir, now guarding empty inputs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rWmR6NS47FpedJsrTWk8A

* fix: repair umask-clamped modes on system shims and managed config

os.WriteFile and os.MkdirAll honor the process umask, so a hardened root
umask (e.g. 077) produced 0700 shims other users cannot execute and a
0600/0700 managed config non-root pmg runs cannot read - silently
disabling the system-wide policy. Chmod/chown the artifacts explicitly
after writing, with unix regression tests running under umask 077.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rWmR6NS47FpedJsrTWk8A

* refactor: address system-install maintainability review

- move PathWithinDir and the root-owned mkdir/chmod helpers into a new
  internal/fsutil package; config no longer exports a generic fs util
- create managed-config directories with MkdirAllRootOwned, which only
  sets ownership and mode on directories it creates - drops the
  hardcoded safedep parent-dir heuristic and never touches pre-existing
  directories
- collapse NewSystemShimManagerForRemove into NewSystemShimManager and
  validate the executable in Install, where the action needs it; Remove
  works regardless of binary state
- rename ShimConfig.ManageProfile to SystemProfile and document it
- name the Linux-only system paths linuxSystemBinDir/ProfilePath and
  document the Unix-only validation semantics
- share the PMG_BIN shim variable name between writeShimScript and
  parseShimPMGBin via the shimPMGBinVar constant
- cloud attribution falls back to the effective uid when SUDO_UID is
  absent, so sudo-invoked commands are not misattributed to root

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rWmR6NS47FpedJsrTWk8A

---------

Co-authored-by: Sahilb315 <bansalsahil315@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants